Understanding Deadlocks, Thread Safety, and ExecutorService Fundamentals
Course: Core Java Training
Topic: Deadlocks, Thread Safety, and ExecutorService Fundamentals
Course Link: Core Java Training Course
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction to Java Concurrency
Java concurrency allows multiple tasks to make progress during overlapping periods of execution. It is an important concept in modern application development because applications frequently perform database operations, process requests, access shared resources, and execute background tasks simultaneously.
Java provides multithreading support through the Thread class, the Runnable and Callable interfaces, synchronization mechanisms, and the java.util.concurrent package.
However, concurrent programming introduces challenges. Multiple threads may access the same data, compete for locks, or wait for resources held by one another. Developers must understand deadlocks and thread safety to avoid these problems.
The ExecutorService API provides a higher-level way to manage task execution through thread pools, task submission, result retrieval, cancellation, and controlled shutdown.
Learning Objectives
- Understand Java concurrency and multithreading fundamentals.
- Define deadlock and identify its necessary conditions.
- Learn how deadlocks occur and how to prevent them.
- Understand thread safety and shared mutable state.
- Use synchronization, atomic variables, and concurrent collections appropriately.
- Understand the Executor framework and thread pools.
- Use Runnable, Callable, Future, and ExecutorService.
- Manage task execution, exceptions, cancellation, and executor shutdown.
- Apply concurrency concepts to practical Java applications.
2. Understanding Threads and Concurrent Execution
A thread is an independent path of execution within a Java process. Multiple threads can execute different tasks concurrently while sharing process resources, including heap objects.
Example: Creating Threads
class MessageTask implements Runnable {
@Override
public void run() {
System.out.println(
Thread.currentThread().getName()
+ " is executing"
);
}
}
public class ThreadExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MessageTask());
Thread t2 = new Thread(new MessageTask());
t1.start();
t2.start();
}
}
Explanation
Runnable represents a task that does not return a result.
- The
run() method contains the task logic.
- The
Thread constructor receives the task.
- The
start() method starts a new thread of execution.
- The execution order of the two threads is not guaranteed.
Concurrent execution can improve responsiveness and throughput. However, if threads access shared mutable data without suitable coordination, the application may produce incorrect results.
3. What Is a Deadlock?
A deadlock is a situation in which two or more threads become permanently blocked because each is waiting for a resource or lock held by another thread in the group.
Java does not automatically prevent every deadlock. Developers must design their locking strategies carefully to avoid circular dependencies.
Real-World Example
Imagine two people working with two shared resources:
- Person A holds Resource 1 and waits for Resource 2.
- Person B holds Resource 2 and waits for Resource 1.
- Neither person releases the resource they already hold.
- Both remain waiting indefinitely.
The same situation can occur when Java threads acquire multiple object monitors in conflicting orders.
Deadlock Representation
Thread 1:
Acquires Lock A
Waits for Lock B
Thread 2:
Acquires Lock B
Waits for Lock A
Result:
Thread 1 waits for Thread 2
Thread 2 waits for Thread 1
Neither thread can proceed
4. Four Necessary Conditions for Deadlock
Deadlock theory identifies four conditions that must hold simultaneously for a classic resource-allocation deadlock to occur.
4.1 Mutual Exclusion
A resource can be held by only one thread at a time. For example, only one thread can own a particular Java monitor lock at any given moment.
4.2 Hold and Wait
A thread holds at least one resource while waiting to acquire another resource held by a different thread.
4.3 No Preemption
A resource cannot simply be taken away from a thread by another thread. The holder must release the resource voluntarily under the applicable locking mechanism.
4.4 Circular Wait
A circular chain of waiting exists. For example, Thread A waits for a lock held by Thread B, while Thread B waits for a lock held by Thread A.
| Condition |
Meaning |
Possible Prevention Strategy |
| Mutual exclusion |
Resources are exclusively owned |
Use nonexclusive access where the resource and requirements permit it |
| Hold and wait |
A thread holds one resource while requesting another |
Acquire required resources together where practical or avoid nested locking |
| No preemption |
Resources are not forcibly taken from their holders |
Use interruptible or timed lock acquisition when suitable |
| Circular wait |
Threads wait in a cycle |
Acquire locks in a consistent global order |
In practical Java applications, consistent lock ordering is one of the most useful ways to reduce deadlock risk.
5. Deadlock Example in Java
The following program demonstrates a possible deadlock caused by two threads acquiring two locks in opposite orders.
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1 acquired Lock A");
sleepBriefly();
synchronized (lockB) {
System.out.println("Thread 1 acquired Lock B");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2 acquired Lock B");
sleepBriefly();
synchronized (lockA) {
System.out.println("Thread 2 acquired Lock A");
}
}
});
t1.start();
t2.start();
}
private static void sleepBriefly() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Possible Output
Thread 1 acquired Lock A
Thread 2 acquired Lock B
The program may stop making progress after these messages if each thread obtains its first lock before trying to acquire the second. The precise behavior depends on thread scheduling.
Why Does Deadlock Occur?
- Thread 1 acquires
lockA.
- Thread 2 acquires
lockB.
- Thread 1 requests
lockB, which Thread 2 holds.
- Thread 2 requests
lockA, which Thread 1 holds.
- Both threads remain blocked while waiting for the other lock.
Important: The sleep call makes the problematic interleaving easier to observe but does not guarantee that deadlock will occur in every execution.
6. How to Prevent Deadlocks
6.1 Use a Consistent Lock Order
The simplest strategy is to acquire multiple locks in the same order across all code paths.
public class SafeLockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void performTask() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("Task completed safely");
}
}
}
}
Any other method requiring both locks should also acquire lockA before lockB. Merely changing one method is insufficient if another method still uses the opposite order.
6.2 Avoid Unnecessary Nested Locks
Reduce the number of locks a thread must hold simultaneously. Where possible, complete independent operations separately instead of nesting multiple synchronized sections.
6.3 Use tryLock() with a Timeout
The ReentrantLock class supports timed and interruptible lock acquisition. If a thread cannot acquire a required lock within a chosen timeout, it can release locks already acquired and retry or report failure.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() throws InterruptedException {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("Lock acquired");
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock");
}
}
}
This example demonstrates timed acquisition of a single lock. Using timeouts for multiple locks requires careful cleanup of every lock already acquired; a timeout alone does not guarantee deadlock-free design.
6.4 Avoid Holding Locks During Slow Operations
Avoid performing network calls, database operations, or long-running tasks while holding locks unless the design specifically requires it. Long-held locks increase contention and can make lock cycles more likely.
6.5 Use Interruption and Cancellation Responsibly
Interruptible operations allow tasks to respond to cancellation requests. However, interrupting a thread does not automatically release every monitor or solve a deadlock involving intrinsic locks.
7. How to Detect and Debug Deadlocks
Deadlocks can be difficult to identify because a program may appear frozen without throwing an exception.
Useful Debugging Techniques
- Take a Java thread dump when the application stops making progress.
- Inspect threads in the
BLOCKED state and identify the monitors they are waiting to acquire.
- Check which threads own the required locks.
- Use tools such as
jstack or Java monitoring tools to inspect thread activity.
- Review code paths that acquire multiple locks in different orders.
- Use timeouts and diagnostic logging around lock acquisition where appropriate.
- Reproduce the issue with concurrent tests and realistic workloads.
Example Diagnostic Command
jstack <process-id>
Replace <process-id> with the Java application's process ID. Depending on the JVM and diagnostic output, a thread dump may identify a deadlock and show the involved threads and locks.
8. What Is Thread Safety?
Thread safety means that a class, method, or component behaves correctly when accessed concurrently by multiple threads according to its documented contract.
A thread-safe component protects its internal state against invalid concurrent interactions. The appropriate strategy depends on how the data is shared and what operations must remain consistent.
Examples of Thread-Safe Design
- Using synchronized methods to protect shared mutable state.
- Using atomic classes for individual atomic updates.
- Using concurrent collections for shared data structures.
- Designing immutable objects that cannot change after construction.
- Keeping mutable data local to one thread.
Why Thread Safety Matters
- Prevents lost updates and inconsistent state.
- Helps maintain application correctness under concurrent load.
- Reduces unpredictable failures that are difficult to reproduce.
- Supports reliable background processing and concurrent request handling.
- Improves confidence in shared components used across an application.
9. Thread Safety and Shared Mutable Data
Shared mutable data is data that can be accessed by multiple threads and changed during execution. It is a common source of concurrency bugs.
Unsafe Example
class UnsafeCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
The increment() method is unsafe when several threads call it concurrently on the same object. The ordinary increment operation is not atomic.
Thread-Safe Version Using synchronized
class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
The synchronized methods use the same object's monitor. This protects the increment operation and coordinates reads through the same monitor.
Thread-Safe Version Using AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
AtomicInteger is useful for independent integer updates. For operations involving multiple variables or a sequence of dependent steps, additional coordination may still be necessary.
10. Strategies for Achieving Thread Safety
10.1 Synchronization
Use the synchronized keyword to protect critical sections guarded by the same monitor.
public synchronized void updateBalance(double amount) {
balance += amount;
}
10.2 Atomic Classes
Atomic classes provide operations on individual values that are performed atomically.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
10.3 Immutable Objects
An immutable object cannot have its state changed after construction. Properly designed immutable objects are naturally easier to share safely across threads.
final class Employee {
private final String name;
private final int id;
public Employee(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
}
This class has final fields and no mutator methods. For objects containing references to mutable data, immutability may also require defensive copying and avoiding exposure of mutable internals.
10.4 Concurrent Collections
Concurrent collections are designed for safe concurrent access. Examples include ConcurrentHashMap and BlockingQueue.
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> scores =
new ConcurrentHashMap<>();
scores.put("Java", 95);
scores.put("SQL", 90);
Concurrent collections protect their own operations, but a sequence of multiple operations may still require additional coordination if the sequence must be atomic.
10.5 Thread Confinement
Thread confinement means that mutable data is accessed by only one thread. Avoiding shared mutable state often eliminates the need for synchronization.
10.6 Volatile Variables
The volatile keyword provides visibility and ordering guarantees for a field across threads. It does not make compound operations such as count++ atomic and does not provide mutual exclusion.
11. What Is the Executor Framework?
The Executor framework is part of Java's java.util.concurrent package. It separates task submission from the details of how tasks are executed.
Instead of manually creating a new Thread for every task, an application can submit tasks to an executor that manages execution resources.
Main Benefits
- Separates task logic from thread management.
- Supports reusable worker threads through thread pools.
- Allows tasks to be queued and executed concurrently.
- Supports returning results through
Future.
- Provides task cancellation and lifecycle management.
- Can improve resource management when configured appropriately.
The Java concurrency APIs provide executor implementations, scheduling support, futures, and other coordination utilities. Read the Java concurrency API documentation.
12. Understanding Executor, ExecutorService, and ScheduledExecutorService
| Interface |
Purpose |
Common Methods |
Executor |
Defines a basic task-execution abstraction |
execute() |
ExecutorService |
Supports task submission, result tracking, and controlled shutdown |
submit(), invokeAll(), shutdown() |
ScheduledExecutorService |
Supports delayed and periodic task execution |
schedule(), scheduleAtFixedRate() |
Executor
The Executor interface provides the execute(Runnable) method. It defines how a task is submitted without requiring the caller to manage the underlying thread directly.
ExecutorService
ExecutorService extends Executor and adds methods for submitting tasks, retrieving results, coordinating task completion, and shutting down the service.
ScheduledExecutorService
ScheduledExecutorService extends ExecutorService and supports tasks that run after a delay or periodically.
13. What Is a Thread Pool?
A thread pool is a collection of worker threads that execute submitted tasks. Instead of creating a new thread for every operation, an executor can reuse available workers.
How a Thread Pool Works
- The application creates an executor.
- The application submits tasks to the executor.
- The executor assigns tasks to available worker threads.
- If all workers are busy, additional tasks may wait in a queue or be rejected, depending on the executor configuration.
- When a worker finishes a task, it can process another task.
- The application shuts down the executor when it is no longer needed.
Advantages of Thread Pools
- Reduces repeated thread-creation overhead.
- Allows control over the number of concurrently executing tasks.
- Provides a centralized place to manage task execution.
- Supports queued work and task completion tracking.
- Can improve application responsiveness and resource utilization.
A thread pool does not automatically make task code thread-safe. If multiple tasks modify the same object, the code must still protect that shared state.
14. Creating an ExecutorService
The Executors utility class provides factory methods for creating commonly used executor implementations.
Common Factory Methods
| Method |
Description |
Typical Use |
newFixedThreadPool(n) |
Creates a pool with a fixed number of worker threads |
Controlling concurrent worker count |
newSingleThreadExecutor() |
Uses one worker to execute tasks sequentially |
Serial task processing |
newCachedThreadPool() |
Creates and reuses threads as needed |
Short-lived asynchronous tasks with suitable workload controls |
newScheduledThreadPool(n) |
Creates a scheduler with a core pool size |
Delayed and periodic jobs |
Resource consideration: Some factory-created executors use unbounded queues or allow substantial thread growth. For production workloads with unpredictable task volumes, consider a deliberately configured ThreadPoolExecutor with suitable queue capacity, thread limits, and rejection handling.
15. Example: Using ExecutorService with Runnable
The Runnable interface represents a task that does not return a result.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorRunnableDemo {
public static void main(String[] args) {
ExecutorService executor =
Executors.newFixedThreadPool(2);
try {
Runnable task1 = () ->
System.out.println(
"Task 1: " +
Thread.currentThread().getName()
);
Runnable task2 = () ->
System.out.println(
"Task 2: " +
Thread.currentThread().getName()
);
executor.execute(task1);
executor.execute(task2);
} finally {
executor.shutdown();
}
}
}
Explanation
- A fixed thread pool with two workers is created.
- Two Runnable tasks are defined.
- The
execute() method submits each task for execution.
- The worker threads run the tasks independently.
shutdown() stops the executor from accepting new tasks while allowing already submitted tasks to complete.
The output order is not guaranteed. The executor may reuse worker threads for multiple tasks.
16. Understanding Callable and Future
Java provides Callable for tasks that return a result and may throw checked exceptions. The Future interface represents the result of an asynchronous computation.
Runnable vs. Callable
| Feature |
Runnable |
Callable |
| Return value |
No result from the task method |
Returns a value of type V |
| Method |
run() |
call() |
| Checked exceptions |
Cannot declare arbitrary checked exceptions in run() |
Can declare checked exceptions |
| Common submission method |
execute() or submit() |
submit() |
| Result tracking |
Via a Future when submitted using a suitable submit method |
Typically through a Future returned by submit |
Example: Callable and Future
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableFutureDemo {
public static void main(String[] args)
throws Exception {
ExecutorService executor =
Executors.newSingleThreadExecutor();
try {
Callable<Integer> task = () -> {
int a = 20;
int b = 30;
return a + b;
};
Future<Integer> future =
executor.submit(task);
Integer result = future.get();
System.out.println("Result: " + result);
} finally {
executor.shutdown();
}
}
}
Output
Result: 50
Explanation
- The Callable task calculates the sum of two numbers.
submit() schedules the task and returns a Future.
future.get() waits if necessary for completion and retrieves the result.
- If the task fails,
get() reports the failure through an ExecutionException.
- The executor is shut down in the finally block.
Important: Calling get() without a timeout can wait indefinitely if the task never completes. Use a timed get() when the application requires a deadline.
17. Important ExecutorService Methods
| Method |
Purpose |
execute(Runnable) |
Submits a Runnable for execution without returning a Future |
submit(Runnable) |
Submits a Runnable and returns a Future representing completion |
submit(Callable) |
Submits a result-producing task and returns a Future |
invokeAll() |
Submits a collection of tasks and waits for all to finish, subject to the applicable timeout or interruption |
invokeAny() |
Returns the successful result of one completed task, or throws if no task succeeds |
shutdown() |
Stops accepting new tasks and allows submitted tasks to finish |
shutdownNow() |
Attempts to stop active tasks and returns tasks that have not started |
awaitTermination() |
Waits for termination for a specified period |
isShutdown() |
Checks whether shutdown has been initiated |
isTerminated() |
Checks whether all tasks have completed after shutdown |
18. ExecutorService Shutdown and Lifecycle Management
Executors use resources that must be managed carefully. A service that is no longer needed should be shut down so its worker threads and other resources can be released.
Graceful Shutdown Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorShutdownDemo {
public static void main(String[] args)
throws InterruptedException {
ExecutorService executor =
Executors.newFixedThreadPool(2);
try {
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() ->
System.out.println(
"Processing task " + taskId
)
);
}
} finally {
executor.shutdown();
}
try {
if (!executor.awaitTermination(
5, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(
5, TimeUnit.SECONDS)) {
System.err.println(
"Executor did not terminate"
);
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Explanation
- Five tasks are submitted to a pool of two workers.
shutdown() rejects new tasks but allows accepted tasks to complete.
awaitTermination() waits for the executor to terminate for up to the specified duration.
shutdownNow() makes a best-effort attempt to stop active work and returns tasks that never commenced.
- Interruption is handled by requesting shutdown and restoring the current thread's interrupt status.
Note: shutdownNow() does not forcibly terminate arbitrary Java code. Tasks need to cooperate with interruption or another cancellation mechanism for prompt termination.
19. Exception Handling with ExecutorService
Task exceptions must be handled according to how the task is submitted.
Using submit()
When a task is submitted using submit(), its failure is generally captured by the returned Future. Calling get() reports the failure through an ExecutionException.
import java.util.concurrent.*;
public class FutureExceptionDemo {
public static void main(String[] args) {
ExecutorService executor =
Executors.newSingleThreadExecutor();
try {
Future<Integer> future =
executor.submit(() -> {
throw new IllegalStateException(
"Task failed"
);
});
try {
System.out.println(future.get());
} catch (ExecutionException e) {
System.out.println(
"Task error: " + e.getCause()
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} finally {
executor.shutdown();
}
}
}
Important Exception Types
ExecutionException: Indicates that a task failed while retrieving its result.
InterruptedException: Indicates that a waiting operation was interrupted.
CancellationException: Indicates that the Future was cancelled.
RejectedExecutionException: May occur when a task cannot be accepted by the executor.
20. Task Cancellation and Timeouts
Long-running tasks should have appropriate cancellation and timeout strategies, especially when they involve external services or potentially blocking operations.
Example: Timed Result Retrieval
import java.util.concurrent.*;
public class FutureTimeoutDemo {
public static void main(String[] args) {
ExecutorService executor =
Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "Completed";
});
try {
System.out.println(
future.get(1, TimeUnit.SECONDS)
);
} catch (TimeoutException e) {
System.out.println("Task is taking too long");
future.cancel(true);
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.out.println("Task failed");
} finally {
executor.shutdownNow();
}
}
}
Explanation
- The task sleeps for three seconds before returning a result.
- The main thread waits for at most one second for the result.
- If the timeout expires, the Future can be cancelled.
cancel(true) requests interruption of the task if it is running.
- The task must respond appropriately to interruption for cancellation to work promptly.
21. ExecutorService vs. Creating Threads Manually
| Feature |
Manual Thread Creation |
ExecutorService |
| Task submission |
Usually requires creating and starting Thread objects |
Submit tasks to an executor |
| Thread reuse |
Must be designed and managed separately |
Thread pools can reuse workers |
| Result retrieval |
Requires additional coordination |
Future supports result retrieval |
| Task management |
Mostly application-managed |
Provides task submission and lifecycle APIs |
| Concurrency limits |
Must be implemented separately |
Can be configured through suitable executor implementations |
| Shutdown management |
Manage thread lifecycle manually |
Offers shutdown and termination methods |
ExecutorService is generally a convenient choice for managing collections of concurrent tasks. Manual thread creation remains useful for learning thread fundamentals and for specialized use cases.
22. Thread Safety and ExecutorService: Important Differences
ExecutorService manages task execution; it does not automatically protect shared application data.
class SharedCounter {
private int count = 0;
public void increment() {
count++;
}
}
Submitting calls to this class through an executor does not make its increment() method thread-safe.
Safer Version
import java.util.concurrent.atomic.AtomicInteger;
class SharedCounter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
The atomic counter supports concurrent increments safely. In real applications, choose a concurrency strategy based on the complete operation being protected, not just on the fact that tasks execute through a thread pool.
23. Common Concurrency Problems
| Problem |
Description |
Possible Solution |
| Deadlock |
Threads wait indefinitely for locks held by one another |
Consistent lock ordering and careful lock design |
| Race condition |
Results depend on timing of concurrent operations |
Synchronization, atomic operations, or other suitable coordination |
| Starvation |
A thread repeatedly fails to obtain resources or execution time |
Review scheduling, lock fairness, and critical-section duration |
| Livelock |
Threads remain active but fail to make useful progress |
Change retry policies or use backoff strategies |
| Excessive contention |
Many threads compete for the same resource |
Reduce shared state and shorten critical sections |
| Resource exhaustion |
Too many threads or queued tasks consume available resources |
Use suitable thread limits, queue capacities, and rejection policies |
24. Best Practices for Java Concurrency
- Identify all shared mutable state before implementing concurrent tasks.
- Prefer immutable objects and thread confinement when possible.
- Protect complete logical operations using a consistent locking strategy.
- Acquire multiple locks in a consistent order.
- Keep critical sections short and avoid unnecessary nested locks.
- Use atomic classes for appropriate individual atomic operations.
- Choose concurrent collections when their behavior fits the use case.
- Use ExecutorService instead of manually creating large numbers of threads for routine task management.
- Configure thread pools according to workload, available resources, and latency requirements.
- Handle task exceptions and interruption correctly.
- Always establish an appropriate executor shutdown strategy.
- Use timeouts for operations that might otherwise wait indefinitely.
- Test concurrency behavior under realistic load and varied scheduling conditions.
- Use thread dumps and monitoring tools to investigate hangs and contention.
25. Hands-On Practice Exercises
Exercise 1: Create a Deadlock
Create two shared lock objects and two threads that acquire them in opposite orders. Observe the possible deadlock and explain how circular wait occurs.
Exercise 2: Prevent Deadlock
Modify the previous program so that both threads acquire locks in the same order. Run the program repeatedly to verify that the lock-ordering problem has been removed.
Exercise 3: Build a Thread-Safe Counter
Implement a counter using an ordinary integer, a synchronized method, and AtomicInteger. Run multiple tasks and compare the results.
Exercise 4: Execute Tasks with ExecutorService
Create a fixed thread pool and submit ten Runnable tasks. Print each task identifier and worker thread name.
Exercise 5: Use Callable and Future
Submit a Callable that returns a calculation result. Retrieve the result using Future and handle possible exceptions.
Exercise 6: Manage Executor Shutdown
Submit multiple tasks, call shutdown, and use awaitTermination to wait for completion. Add a fallback cancellation strategy.
Exercise 7: Handle Task Failure
Submit a task that throws an exception. Observe how the exception is reported when retrieving its Future result.
Exercise 8: Implement a Task Timeout
Submit a task that takes several seconds to finish. Use timed Future.get and implement a cancellation policy.
26. Interview Questions and Answers
Q1. What is a deadlock in Java?
Answer: A deadlock occurs when two or more threads wait indefinitely for resources or locks held by one another, preventing progress.
Q2. What are the four necessary conditions for a classic deadlock?
Answer: Mutual exclusion, hold and wait, no preemption, and circular wait.
Q3. How can deadlocks be prevented?
Answer: Use a consistent lock order, reduce nested locking, consider timed or interruptible acquisition where suitable, and avoid holding locks unnecessarily.
Q4. What is thread safety?
Answer: Thread safety means a component maintains correct behavior when accessed concurrently according to its contract.
Q5. Does ExecutorService automatically make code thread-safe?
Answer: No. ExecutorService manages task execution, but shared mutable state must still be protected appropriately.
Q6. What is the Executor framework?
Answer: It is a Java concurrency framework that separates task submission from the details of task execution and thread management.
Q7. What is the difference between Executor and ExecutorService?
Answer: Executor provides basic task execution through execute(). ExecutorService adds task submission, Future-based result tracking, and lifecycle management.
Q8. What is a thread pool?
Answer: A thread pool manages worker threads that can be reused to execute multiple submitted tasks.
Q9. What is the difference between Runnable and Callable?
Answer: Runnable's run() method does not return a task result. Callable's call() method returns a value and can throw checked exceptions.
Q10. What is a Future in Java?
Answer: Future represents the result of an asynchronous computation and provides methods to retrieve a result, check completion, or request cancellation.
Q11. What is the difference between shutdown() and shutdownNow()?
Answer: shutdown() stops accepting new tasks and allows submitted tasks to finish. shutdownNow() attempts to stop active tasks and returns tasks that have not started.
Q12. What is AtomicInteger?
Answer: AtomicInteger provides atomic operations on a single integer value and is useful for concurrent counters.
Q13. Can a synchronized program still experience deadlock?
Answer: Yes. Synchronization protects critical sections, but acquiring multiple locks in an inconsistent order can cause deadlock.
Q14. What is starvation?
Answer: Starvation occurs when a thread repeatedly fails to obtain the resources or execution time needed to make progress.
Q15. Why should an ExecutorService be shut down?
Answer: Shutdown prevents new task submissions and allows executor resources to be released after existing work is handled.
27. Quick Revision Checklist
- Deadlock occurs when threads wait for resources held by one another.
- The four classic deadlock conditions are mutual exclusion, hold and wait, no preemption, and circular wait.
- Consistent lock ordering is a key deadlock-prevention strategy.
- Thread safety ensures correct behavior under concurrent access.
- Shared mutable state requires an appropriate concurrency strategy.
- Synchronization and atomic classes can help prevent unsafe concurrent updates.
- Executor separates task submission from execution details.
- ExecutorService supports task submission, Futures, and lifecycle management.
- Thread pools reuse workers to execute multiple tasks.
- Runnable represents a task without a result; Callable can return a result.
- Future supports result retrieval, completion checks, and cancellation requests.
- shutdown allows submitted tasks to complete; shutdownNow attempts to stop ongoing work.
- ExecutorService does not automatically make shared application state thread-safe.
- Concurrency testing, monitoring, and careful resource management are essential.
28. Conclusion
Deadlocks, thread safety, and ExecutorService are important topics in Core Java because they determine how reliably an application behaves when several tasks execute concurrently.
Deadlocks can prevent an application from making progress, while unsafe access to shared mutable data can produce inconsistent results. Developers can reduce these risks by applying consistent lock ordering, protecting shared state correctly, using immutable data where practical, and choosing suitable concurrency utilities.
ExecutorService simplifies task management by providing reusable worker threads, task submission, result tracking, cancellation, and controlled shutdown. However, good concurrency design still requires developers to manage shared state, exceptions, timeouts, and resource usage carefully.
Practice these concepts with counters, banking examples, multiple locks, and executor-based tasks to develop a strong foundation in Java multithreading.
Learn Core Java with JustAcademy
Continue learning Java programming and multithreading through these resources: